PooledIdentifierProvider.java

package org.codefilarete.stalactite.engine.idprovider;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * Simple provider that maintains a set of identifiers to prevent from a time consuming creation of those identifiers.
 * For instance, used for database based generated identifiers since the retrieval of a bunch of values may be time consuming.
 * Thought for table-sequence-generated identifiers like hi-lo ones.
 * 
 * Thread-safe because based on a {@link BlockingQueue}
 * 
 * @author Guillaume Mary
 */
public abstract class PooledIdentifierProvider<T> implements IdentifierProvider<T> {
	
	private final BlockingQueue<T> queue;
	private final int threshold;
	private final Executor executor;
	private final Duration timeOut;
	
	/**
	 * 
	 * @param initialValues the initial values to fill this queue. Can be empty, not null.
	 * @param threshold the threshold below (excluded) which the background service is called to fill this queue
	 * @param executor the executor capable of running a background task of filling this queue
	 * @param timeOut the timeout after which the {@link #giveNewIdentifier()} gives up and returns null because of an empty queue
	 */
	public PooledIdentifierProvider(Collection<T> initialValues, int threshold, Executor executor, Duration timeOut) {
		// we use LinkedBlockingQueue because it's not bounded (with this constructor) : we need it because our implementation
		// of giveNewIdentifier doesn't guarantee the number of elements in the stack
		this.queue = initialValues.stream().collect(Collectors.toCollection(LinkedBlockingQueue::new));
		this.threshold = threshold;
		this.executor = executor;
		this.timeOut = timeOut;
	}
	
	/**
	 * Pops the queue. If there's not any more it will wait for any value until timeout (defined at construction time).
	 */
	private T pop() {
		try {
			return queue.poll(timeOut.getSeconds(), TimeUnit.SECONDS);
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
			// unreachable
			return null;
		}
	}
	
	/**
	 * Warn: may return null if timeout (defined at construction time) is reached when trying to pop this queue
	 * @return the identifier on the "top" of this queue
	 */
	@Override
	public T giveNewIdentifier() {
		// We ask for a queue filling if we reached the given threshold (below which we must refuel)
		// Since this code block is not synchronized, we could have several refueling tasks added to the executor. I don't think it's a problem
		// because the consequence is that we'll get some "extra" values reserved : the drawback is memory consumption due to extra identifiers
		// which should be very small, and of course a extra values can be lost if JVM crashes. 
		// (we do it before pop() to take account of very first call : queue can be empty)
		boolean refueling = ensureMinimalPool();
		T toReturn = pop();
		// we do it again for future calls to pop (if not previously done)
		if (!refueling) {
			ensureMinimalPool();
		}
		return toReturn;
	}
	
	private boolean ensureMinimalPool() {
		if (queue.size() < threshold) {
			executor.execute(this::fillQueue);
			return true;
		} else {
			return false;
		}
	}
	
	private void fillQueue() {
		this.queue.addAll(new ArrayList<>(retrieveSomeValues()));
	}
	
	/**
	 * Should return an unspecified number of values that will be pooled. Implementations are free to give more or less values depending on what
	 * they want !
	 * @return some non null values
	 */
	protected abstract Collection<T> retrieveSomeValues();
}